﻿
using System;
using System.IO;
using System.Drawing;
using System.Collections.Generic;

using GECS.Core;
using GECS.Util;
using GECS.Object;
using GECS.Collision;

namespace GECS.MapEngine {

	/// <summary>
	/// This class represents a map (tile-based of course!) which is currently
	/// playing the game. You could use the MapManager class to control the order of
	/// maps. Note that this class is completely static. A map requires MapInfo
	/// objects if you does't want to load from file (so nobody can edit your levels).
	/// </summary>
    public class Map {

        // MapData used for debugging
        private static char[,] mapdata = null;
        // The tiles
        private static Tile[,] _tiles = null;

        /// <summary>
        /// The Tile Size of the map
        /// </summary>
        public static int TILE_SIZE = 0;

        // Width and height
        private static int MAP_WIDTH = 0;
        private static int MAP_HEIGHT = 0;

        private static ICollisionEngine collider = null;

        private static List<GObject> objects = null;
        private static List<GObject> removed = null;

        private static Dictionary<int, MapLayer> layers = null;

        private static int maxDepth = 0;

        /// <summary>
        /// Initializes the map. Called by the game class.
        /// </summary>
        public static void InitMap() {
            objects = new List<GObject>();
            removed = new List<GObject>();
            layers = new Dictionary<int, MapLayer>();
            collider = new Grid();
        }
        
        /// <summary>
        /// Gets or sets the collision engine to use to resolve collisions.
        /// </summary>
        public ICollisionEngine CollisionEngine {
        	get {
        		return collider;
        	}
        	set {
        		collider = value;
        	}
        }

        /// <summary>
        /// Returns the tile number containing a pixel.
        /// </summary>
        /// <param name="px">The coordinate of the pixel.</param>
        /// <returns>The tile number</returns>
        public static int ToTiles(int px) {
            return TILE_SIZE/px;
        }

        /// <summary>
        /// Returns the first pixel of a tile.
        /// </summary>
        /// <param name="t">The tile number.</param>
        /// <returns>The first pixel number.</returns>
        public static int ToPixels(int t) {
            return TILE_SIZE * t;
        }

        /// <summary>
        /// Returns the bounds of tile at a specified position.
        /// </summary>
        /// <param name="x">The x-position of the tile (in tiles).</param>
        /// <param name="y">The y-position of the tile (in tiles)</param>
        /// <returns>A rectangle with the bounds.</returns>
        public static Rectangle GetTileBounds(int x, int y) {
            return new Rectangle(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
        }

        /// <summary>
        /// Returns the character in the map file at the specified tile position of
        /// the object. (Note that it is only used for debugging).
        /// </summary>
        /// <param name="x">The x-position of the tile.</param>
        /// <param name="y">The y-position of the tile.</param>
        /// <returns></returns>
        public static char GetTileAt(int x, int y) {
            return mapdata[x, y];
        }

        /// <summary>
        /// Loads a map from an array of lines by using the MapLoader.
        /// </summary>
        /// <param name="lines">The array of lines to be parsed.</param>
        /// <param name="loader">The MapLoader to be used.</param>
        public static void LoadMap(string[] lines, MapLoader loader) {
            // Clear objects
            ClearObjects();
            // Reset the map view
            MapView.OffSetX = 0;
            MapView.OffSetY = 0;
            // Reset Map's width and height
            MAP_WIDTH = MAP_HEIGHT = 0;
            // Reset Layering data
            maxDepth = 0;
            // Parse the file
            foreach (string line in lines) {
                MAP_HEIGHT++;
                MAP_WIDTH = Math.Max(MAP_WIDTH, line.Length);
            }
            mapdata = new char[MAP_WIDTH, MAP_HEIGHT];
            _tiles = new Tile[MAP_WIDTH, MAP_HEIGHT];
            for (int i=0; i<lines.Length; i++) {
                string line = lines[i];
                for (int j=0; j<line.Length; j++) {
                    mapdata[j, i] = line[j];
                    GObject obj = loader.GetObject(mapdata[j, i], j*TILE_SIZE, i*TILE_SIZE);
                    if (obj != null) {
                        AddObject(obj);
                    }
                    _tiles[j, i] = loader.GetTile(mapdata[j, i], j*TILE_SIZE, i*TILE_SIZE);
                }
            }
            int mw = Math.Max(Width, Global.WIDTH);
            int mh = Math.Max(Height, Global.HEIGHT);
            collider.SetBounds(0, 0, mw, mh);
        }

        /// <summary>
        /// Loads the map from a MapInfo object.
        /// </summary>
        /// <param name="info">The MapInfo object.</param>
        public static void LoadMap(MapInfo info) {
            string[] lines = info.MapData.Split(new char[] {'\n'});
            TILE_SIZE = info.tileSize;
            LoadMap(lines, info.loader);
        }

        /// <summary>
        /// Loads a map from an external text file.
        /// </summary>
        /// <param name="tsize">The tile size.</param>
        /// <param name="fname">The name of the file.</param>
        /// <param name="loader">The MapLoader object.</param>
        public static void LoadMap(int tsize, string fname, MapLoader loader) {
            string[] lines = File.ReadAllLines(Game.GetFullFileName(fname));
            TILE_SIZE = tsize;
            LoadMap(lines, loader);
        }

        /// <summary>
        /// Clears all objects from the map.
        /// </summary>
        public static void ClearObjects() {
            objects.Clear();
            layers.Clear();
        }

        /// <summary>
        /// Returns the number of instances of a type in the map.
        /// </summary>
        /// <param name="t">The type of the object.</param>
        /// <returns>Number of instances in the map.</returns>
        public static int NumInstances(Type t) {
            int count = 0;
            foreach (GObject obj1 in objects) {
                if (t.IsAssignableFrom(obj1.GetType())) {
                    count++;
                }
            }
            return count;
        }

        /// <summary>
        /// Checks if a specific position is collision free in the map.
        /// </summary>
        /// <param name="x">The x-position of the object.</param>
        /// <param name="y">The y-position of the object.</param>
        /// <param name="solid">Whether to check only for solid objects.</param>
        /// <param name="obj">The object (used for width and height).</param>
        /// <returns>True if no collision found. Else False.</returns>
        public static bool IsObjectCollisionFree(float x, float y, bool solid, GObject obj) {
            bool boolean = true;
            Rectangle bounds = new Rectangle((int)x, (int)y, obj.Width, obj.Height);
            foreach (GObject obj1 in objects) {
                if (obj1.Solid == solid && obj != obj1) {
                    if (obj1.IsAlive()) {
                        if (bounds.IntersectsWith(obj1.Bounds)) {
                            boolean = false;
                            if (Global.USE_PIXELPERFECT_COLLISION) {
                            	boolean = !GUtil.IsPixelPerfectCollision(x, y, obj.Image, obj1.X, obj1.Y, obj1.Image);
                            }
                            break;
                        }
                    }
                }
            }
            return boolean;
        }

        /// <summary>
        /// Checks if a specific position is collision free in the map.
        /// </summary>
        /// <param name="x">The x-position of the object.</param>
        /// <param name="y">The y-position of the object.</param>
        /// <param name="obj">The object (used for width and height).</param>
        /// <returns>True if no collision is found. Else False.</returns>
        public static bool IsObjectCollisionFree(float x, float y, GObject obj) {
            return (IsObjectCollisionFree(x, y, true, obj) || IsObjectCollisionFree(x, y, false, obj));
        }

        /// <summary>
        /// Gets you the list of objects in this map.
        /// </summary>
        public static List<GObject> Objects {
            get {
                return objects;
            }
        }

        /// <summary>
        /// Prints the map to the console. Used to debug while loading maps).
        /// </summary>
        public static void PrintMap() {
            Console.Write(AsString());
        }

        /// <summary>
        /// Returns the current map as a string. Used to debug while loading maps.
        /// </summary>
        /// <returns>The current map as a string.</returns>
        public static string AsString() {
            string mp = "";
            for (int y=0; y<MAP_HEIGHT; y++) {
                for (int x=0; x<MAP_WIDTH; x++) {
                    mp += mapdata[x, y];
                }
                mp += "\n";
            }
            return mp;
        }

        /// <summary>
        /// Renders the map on to a graphics context.
        /// </summary>
        /// <param name="g">The graphics context.</param>
        public static void RenderMap(Graphics g) {
            RenderMap(g, 0, 0);
        }

        /// <summary>
        /// Renders the map at a location on the screen using the graphics context.
        /// </summary>
        /// <param name="g">The graphics context.</param>
        /// <param name="x">The x-position.</param>
        /// <param name="y">The y-position.</param>
        public static void RenderMap(Graphics g, int x, int y) {
            // Layers
            for (int i=maxDepth; i>=0; i--) {
                layers[i].Render(g, x, y);
            }
            // Tiles
            for (int my = 0; my < MAP_HEIGHT; my++) {
                for (int mx = 0; mx < MAP_WIDTH; mx++) {
                    Tile tile = _tiles[mx, my];
                    if (tile != null) {
                        if (MapView.IsVisible(tile)) {
                            g.DrawImage(tile.Image, tile.X, tile.Y);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Updates all the objects in the map.
        /// </summary>
        /// <param name="elapsedTime">The time taken by the previous frame.</param>
        public static void UpdateObjects(long elapsedTime) {
            collider.Clear();
            collider.Insert(objects);
            for (int i=0; i<objects.Count; i++) {
                GObject obj = objects[i];
                if (obj.IsAlive()) {
                    obj.SuperUpdate(elapsedTime);
                    obj.Move();
                    if (obj.IsCollisionListener()) {
                        CheckCollisions(obj);
                    }
                } else {
                    removed.Add(obj);
                }
            }
            // Remove the removed objects
            foreach (GObject obj in removed) {
                objects.Remove(obj);
            }
        }

        /*
         * Checks collisions. Helper method.
         */
        private static void CheckCollisions(GObject obj) {
            List<GObject> collidables = collider.Retrieve(obj);
            foreach (GObject other in collidables) {
                if (other.IsAlive()) {
                    if (other.IsCollidingWith(obj)) {
                        if (other.Solid) {
                            // Move back
                            obj.X = obj.X - obj.VelocityX;
                            obj.Y = obj.Y - obj.VelocityY;
                        }
                        obj.Collision(other);
                    }
                } else {
                    removed.Add(other);
                }
            }
        }

        /// <summary>
        /// Removes all objects of a type from the map.
        /// </summary>
        /// <param name="t">The type of the object.</param>
        public static void RemoveObjectsOfType(Type t) {
            foreach (GObject obj in objects) {
                if (obj.GetType().Equals(t)) {
                    removed.Add(obj);
                }
            }
            // Remove the removed objects
            foreach (GObject obj in removed) {
                objects.Remove(obj);
            }
        }

        /// <summary>
        /// Removes an object from this map.
        /// </summary>
        /// <param name="obj">The object to be removed.</param>
        public static void RemoveObject(GObject obj) {
            objects.Remove(obj);
        }

        /// <summary>
        /// Adds an object to the map.
        /// </summary>
        /// <param name="obj">The object to be added.</param>
        public static void AddObject(GObject obj) {
            if (obj != null) {
                objects.Add(obj);
                maxDepth = Math.Max(maxDepth, obj.Depth);
                if (layers.ContainsKey(maxDepth)) {
                    layers[maxDepth].AddObject(obj);
                } else {
                    MapLayer layer = new MapLayer();
                    layer.AddObject(obj);
                    layers.Add(maxDepth, layer);
                }
            }
        }

        /// <summary>
        /// Checks if an object is aligned to the grid.
        /// </summary>
        /// <param name="obj">The object to be checked.</param>
        /// <returns>True if the object is positioned exactly at the left corner of the grid</returns>
        public static bool IsAlignedToGrid(GObject obj) {
            return IsAlignedToGrid(obj.X, obj.Y);
        }

        /// <summary>
        /// Checks if an object is aligned to the grid.
        /// </summary>
        /// <param name="x">The x-position.</param>
        /// <param name="y">The y-position.</param>
        /// <returns>True if the point is left corner of any cell in the grid.</returns>
        public static bool IsAlignedToGrid(float x, float y) {
            return (x % TILE_SIZE == 0) && (y % TILE_SIZE == 0);
        }

        /// <summary>
        /// Gets the width of this map in pixels.
        /// </summary>
        public static int Width {
            get {
                return MAP_WIDTH * TILE_SIZE;
            }
        }

        /// <summary>
        /// Gets the height of this map in pixels.
        /// </summary>
        public static int Height {
            get {
                return MAP_HEIGHT * TILE_SIZE;
            }
        }

    }

}
